package im.composer.audio.engine;

import jass.engine.BufferNotAvailableException;

import java.util.logging.Logger;

import org.jaudiolibs.audioservers.AudioConfiguration;
import org.tritonus.share.sampled.AudioUtils;
import org.tritonus.share.sampled.FloatSampleBuffer;

/**
 * Output-only unit. Will produce audio-rate buffers. Needs only implementation
 * of computeBuffer().
 * 
 * @author Kees van den Doel (kvdoel@cs.ubc.ca)
 */

public abstract class Out implements Source {

	private transient boolean started = false;
	private transient AudioConfiguration context;
	protected transient double millisecond_per_frame;
	/**
	 * The current time of this object, measured in number of frames of length
	 * bufferSize. So this is the number of frames processed.
	 */
	private transient long currentTime;
	private int sampleOffset = 0;

	/** Buffer length of processed audio buffers. */
	protected transient int bufferSize;

	/** The current buffer. */
	protected transient FloatSampleBuffer buf;

	/** The old buffer. */
	protected transient FloatSampleBuffer bufOld;

	/** To provide access to the old buffer without locking whole class */
	protected transient Object lock;

	protected void copyToOld() {
		if (lock == null) {
			lock = new Object();
		}
		synchronized (lock) {
			try {
				buf.copyTo(bufOld, 0, bufferSize);
			} catch (Exception e) {
			}
		}
	}

	/**
	 * Create at time 0 (which you may want to change by calling setTime() if
	 * objects are created in the middle of some jass.sis process).
	 */
	public Out(AudioConfiguration context) {
		setContext(context);
		setTime(0);
	}

	/**
	 * Create. Does not allocate bufferSize or set time which is still unknown
	 * yet if objects are created in the middle of some jass.sis process).
	 */
	public Out() {
	}


	public AudioConfiguration getContext() {
		return context;
	}

	/**
	 * 设置本输出单元的音频环境。此方法不抛出异常，应当于初始化本输出单元时调用。初始化后，音频环境若有变更，应当调用configure()方法。
	 * 
	 * @param context
	 */
	public void setContext(AudioConfiguration context) {
		this.context = context;
		if (context != null) {
			millisecond_per_frame = AudioUtils.frames2MillisD(context.getMaxBufferSize(), context.getSampleRate());
			setBufferSize(context.getMaxBufferSize());
			clearBuffer();
		}
	}

	@Override
	public synchronized void configure(AudioConfiguration context) throws Exception {
		setContext(context);
	}

	/**
	 * Get current time.
	 * 
	 * @return current time.
	 */
	public synchronized long getTime() {
		return currentTime;
	}

	/**
	 * Set current time. Usually called only at init time.
	 * 
	 * @param t
	 *            current time.
	 */
	public synchronized void setTime(long t) {
		currentTime = t;
		if(t==0){
			started = false;
		}
		notify();
	}

	/**
	 * Set current time and notify waiting threads (used by ThreadMixer).
	 * 
	 * @param t
	 *            current time.
	 */
	public synchronized void setTimeAndNotify(long t) {
		currentTime = t;
		if(t==0){
			started = false;
		}
		notifyAll();
	}

	/**
	 * Reset time of self and all inputs
	 * 
	 * @param t
	 *            time to reset to. Patch must be in a state s.t. none of the
	 *            current times == t
	 */
	public synchronized void resetTime(long t) {
		setTime(t);
	}

	/**
	 * Get buffer size.
	 * 
	 * @return buffer size in samples.
	 */
	public int getBufferSize() {
		return bufferSize;
	}

	/**
	 * Set buffer size. Will also reallocate buffer and clear.
	 * 
	 * @param bufferSize
	 *            buffer size.
	 */
	public void setBufferSize(int bufferSize) {
		this.bufferSize = bufferSize;
		int channels = getContext().getOutputChannelCount();
		if (buf == null) {
			buf = new FloatSampleBuffer(channels, bufferSize, context.getSampleRate());
		} else {
			buf.setSampleCount(bufferSize, true);
		}
		if (bufOld == null) {
			bufOld = new FloatSampleBuffer(channels, bufferSize, context.getSampleRate());
		} else {
			bufOld.setSampleCount(bufferSize, true);
		}
		clearBuffer();
	}

	public int getSampleOffset() {
		return sampleOffset;
	}

	public void setSampleOffset(int sampleOffset) {
		this.sampleOffset = sampleOffset;
	}

	/**
	 * Compute the next buffer and store in member float[] buf. This is the core
	 * processing method which will be implemented for each generator.
	 */
	protected abstract void computeBuffer();

	/**
	 * Clears buffer to zero.
	 */
	public void clearBuffer() {
		buf.makeSilence();
	}

	/**
	 * Get buffer with frame index t. Return old buffer if have it in cache.
	 * Compute next buffer and advance time if requested, throw exception if
	 * requested buffer lies in the past or future. This method will be called
	 * "behind the scenes" when processing filtergraphs.
	 * 
	 * @param t
	 *            timestamp of buffer = frame index.
	 */
	public synchronized FloatSampleBuffer getBuffer(long t) throws BufferNotAvailableException {
		copyToOld();
		if ((currentTime == 0 && !started) || t == currentTime + 1) { // requested
																		// next
																		// buffer
			setTime(t);
			prepareBuffer();
		} else if (t != currentTime) { // neither current or next buffer
										// requested: deny request
			Logger.getAnonymousLogger().severe("Error! " + this + " Out.java: t=" + t + " currentTime=" + currentTime);
			throw new BufferNotAvailableException();
		}
		started = true;
		// return new or old buffer:
		return buf;
	}
	
	protected void prepareBuffer(){
		computeBuffer();
	}

	/**
	 * Get old buffer in cache. Deliberately not synchronized.
	 */
	public FloatSampleBuffer getBuffer() throws BufferNotAvailableException {
		synchronized (lock) {
			return bufOld;
		}
	}

}
